﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net;
using System.IO;
using System.Windows.Browser;
using System.Net.Browser;
//using System.Web;

namespace slTestingStreaming
{
    public class StreamClient : IDisposable 
    {
        #region Static Management
        //private static Mutex streamMutex = new Mutex();
        private static Dictionary<string, StreamClient> clients;


        public static StreamClient GetStreamClient(string username, string password)
        //public StreamClient GetStreamClient(string username, string password)
        {
            bool locked = false;

            try
            {
                //locked = streamMutex.WaitOne();

                if (clients == null)
                {
                    clients = new Dictionary<string, StreamClient>();
                }

                if (clients.ContainsKey(username))
                {
                    return clients[username];
                }

                StreamClient client = new StreamClient(username, password);
                clients.Add(username, client);

                return client;
            }
            finally
            {
                if (locked)
                {
                    //streamMutex.ReleaseMutex();
                }
            }

        }
        #endregion 

        public event EventHandler HasConnected;
        public event EventHandler FailedConnection;
        
        private const string TWITTER_STREAM_API_FILTER_URL = "http://stream.twitter.com/1/statuses/filter.json?";
        private const int TWITTER_STREAM_API_RECONNECT_MINTIME_SECONDS = 120;
        private const int TWITTER_STREAM_API_BACKOFF_INCREMENT_POW_SECONDS = 30;
        private const int TWITTER_STREAM_API_BACKOFF_MAX_SECONDS = 300;

        private const char TWITTER_STREAM_API_FILTER_DELIMITER = '\r';


        public string Username { get; private set; }
        public string Password { get; private set; }

        public bool Connected { get; private set; }

        public int ConnectFailures { get; private set; }
        public DateTime LastConnectFailure { get; private set; }

        public DateTime LastStreamConnect { get; private set; }
        public bool StreamTrackDirty { get; private set; }

        /// <summary>
        /// The tracking API requires that you do not change/reconnect 
        /// </summary>
        public TimeSpan NextStreamChangeWindowOpens
        {
            get
            {
                // Initial state. 
                if (!Connected && LastConnectFailure == DateTime.MinValue && LastStreamConnect == DateTime.MinValue && ConnectFailures == 0)
                {
                    return new TimeSpan(0, 0, 0, 0);
                }


                // No Failures since last successful connect, and last connect was > 2 mins ago. 
                if (ConnectFailures == 0 && LastStreamConnect.AddSeconds(TWITTER_STREAM_API_RECONNECT_MINTIME_SECONDS) < DateTime.UtcNow)
                {
                    return new TimeSpan(0, 0, 0, 0);
                }

                // If ConnectFailures, then auto-backoff.

                if (ConnectFailures > 0 && LastConnectFailure > DateTime.MinValue)
                {
                    // Calc Backoff Value

                    TimeSpan backoff_min = TimeSpan.FromSeconds(Math.Pow((double)TWITTER_STREAM_API_BACKOFF_INCREMENT_POW_SECONDS, (double)ConnectFailures));
                    if (backoff_min.TotalSeconds > TWITTER_STREAM_API_BACKOFF_MAX_SECONDS)
                    {
                        backoff_min = TimeSpan.FromSeconds(TWITTER_STREAM_API_BACKOFF_MAX_SECONDS);
                    }

                    return LastConnectFailure.AddSeconds(backoff_min.Seconds).Subtract(DateTime.UtcNow);
                }

                // No connect failures, the user just wants to reconnect. 
                return LastStreamConnect.AddSeconds(TWITTER_STREAM_API_RECONNECT_MINTIME_SECONDS).Subtract(DateTime.UtcNow);
            }
        }


        public List<string> TrackingKeywords { get; set; }
        public List<string> FollowingUIDs { get; set; }

        private HttpWebRequest Request;
        private HttpWebResponse Response;
        private Stream ResponseStream;
        private StreamReader ResponseStreamReader;
        private Thread WatchThread;

        public delegate void NewStreamMessage(StreamClient client, string message);
        public event NewStreamMessage NewStreamMessageEvent;

        public delegate void StreamDisconnected(StreamClient client);
        public event StreamDisconnected StreamDisconnectedEvent;

        private bool Disposing = false;
        private bool DisconnectRequested = false;

        [Obsolete("Must provide username/password", true)]
        private StreamClient()
        {

        }

        private StreamClient(string username, string password)
        {
            Username = username;
            Password = password;
            TrackingKeywords = new List<string>();
            FollowingUIDs = new List<string>();
            LastStreamConnect = DateTime.MinValue;
            LastConnectFailure = DateTime.MinValue;
        }


        #region Connection Process
        public string Connect()
        {

            #region SETUP FILTERS
            if (NextStreamChangeWindowOpens.TotalSeconds > 0)
            {
                //Logging.Fail("Twitter Streaming API Imposes minimum 2minute window between changes. Don't hammer the API. It's bad, mmkay?. Also, Twitter will ban/block your account if you keep retrying. Seconds until OK to reconnect: {0}", NextStreamChangeWindowOpens.TotalSeconds);
                return string.Empty;
            }

            if (TrackingKeywords.Count == 0 && FollowingUIDs.Count == 0)
            {
                //Logging.Fail("Must provide at least one tracking keyword, or following UID");
                return string.Empty;
            }

            //Disconnect();

            string api_url = TWITTER_STREAM_API_FILTER_URL;
            //string api_url = string.Empty;

            if (TrackingKeywords.Count > 0)
            {
                api_url += "track=";
                foreach (string word in TrackingKeywords.Take(200))
                {
                    api_url += HttpUtility.UrlEncode(word.Replace(",", "")) + ",";
                }
                api_url = api_url.Substring(0, api_url.Length - 1);
                api_url += "&";
            }

            if (FollowingUIDs.Count > 0)
            {
                api_url += "follow=";
                foreach (string uid in FollowingUIDs.Take(400))
                {
                    api_url += uid.ToString() + ",";
                }

                api_url = api_url.Substring(0, api_url.Length - 1);
            }
            #endregion


            this.Request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(new Uri(api_url));

            this.Request.Method = "POST";
            this.Request.Credentials = new NetworkCredential(this.Username, this.Password);
            this.Request.UseDefaultCredentials = false;
            this.Request.AllowReadStreamBuffering = false;

            this.Request.BeginGetResponse(result =>
            {
                try
                {
                    this.Response = (HttpWebResponse)this.Request.EndGetResponse(result);

                    this.ResponseStream = this.Response.GetResponseStream();
                    this.ResponseStreamReader = new StreamReader(this.ResponseStream);

                    StartWatchThread();

                    if (this.HasConnected != null) this.HasConnected(null, null);

                }
                catch (Exception ex) {
                    if (FailedConnection != null) FailedConnection(ex, null);
                }
            }, null);



            return string.Empty;
        }

        #endregion






        private void StartWatchThread()
        {
            try
            {
                if (WatchThread != null)
                {
                    WatchThread.Abort();
                    WatchThread = null;
                }

                WatchThread = new Thread(new ThreadStart(Watchthread_Process));
                WatchThread.Start();
                //Logging.Info("Watcher Started");
            }catch(Exception ex){}
        }

        private void Watchthread_Process()
        {
            bool sendDisconnectEventNotice = true;
            try
            {
                string bufferString = string.Empty;

                //Logging.Info(":::Watcher Thread - Started");
                while (this.ResponseStream != null && this.ResponseStream.CanRead)
                {


                    byte[] data = new byte[4096];
                    int read;
                    while ((read = this.ResponseStream.Read(data, 0, data.Length)) > 0)
                    {
                        string readString = UTF8Encoding.UTF8.GetString(data, 0, read);
                        bufferString = ProcessBuffer(bufferString + readString);
                    }
                }
            }
            catch (ThreadAbortException ex)
            {
                sendDisconnectEventNotice = false;
                //Logging.Fail("Thread Aborted - probably due to Disconnect", ex);
            }
            catch (WebException ex)
            {
                this.LastConnectFailure = DateTime.UtcNow;
                this.ConnectFailures += 1;
                //Logging.Fail("WebException during read. Try and reconnect?", ex);
            }
            finally
            {
                //Logging.Info(":::Watcher Thread - Ended!");

                this.Connected = false;
                if (sendDisconnectEventNotice)
                {
                    SendDisconnectEvent();
                }
            }
        }



        private void SendDisconnectEvent()
        {
            if (!this.Disposing && !this.DisconnectRequested && this.StreamDisconnectedEvent != null)
            {
                //Logging.Info("Sending Disconnect Event: Disposing: {0}, Disconnect Requested {1}", this.Disposing, this.DisconnectRequested);
                this.StreamDisconnectedEvent(this);
            }
        }

        private string ProcessBuffer(string bufferString)
        {
            try
            {
                //Logging.Info("Buffer: '{0}'", bufferString);

                string buffer = bufferString;
                int delimPos = buffer.IndexOf(TWITTER_STREAM_API_FILTER_DELIMITER);

                while (delimPos >= 0)
                {
                    //Logging.Info("Delimiter Pos: " + delimPos); 

                    string message = buffer.Substring(0, delimPos).Replace(TWITTER_STREAM_API_FILTER_DELIMITER.ToString(), "");
                    if (buffer.Length <= delimPos + 1)
                    {
                        buffer = string.Empty;
                    }
                    else
                    {
                        buffer = buffer.Substring(delimPos + 1);
                    }

                    //Logging.Info("Message: '{0}'", message);
                    //Logging.Info("New Buffer: '{0}'", buffer);



                    if (message.Trim().Length > 1 && this.NewStreamMessageEvent != null)
                    {
                        this.NewStreamMessageEvent(this, message);
                    }

                    delimPos = buffer.IndexOf(TWITTER_STREAM_API_FILTER_DELIMITER);
                }

                //Logging.Info("Buffer Size: {0}", buffer.Length);
                return buffer;
            }
            catch (Exception ex) { return string.Empty; }
        }




        public void Disconnect()
        {
            DisconnectRequested = true;

            try
            {
                if (this.WatchThread != null)
                {
                    this.WatchThread.Abort();
                    this.WatchThread.Join(9000);
                    this.WatchThread = null;
                }
            }
            catch (Exception ex){ }


            try{
                if (this.Request != null)
                {
                    this.Request.Abort();
                    this.Request = null;
                }
            }
            catch (Exception ex) { }


            try{
                if (this.Response != null)
                {
                    this.Response.Close();
                    this.Response = null;
                }
            }
            catch (Exception ex) { }


            try{
                if (this.ResponseStream != null)
                {
                    this.ResponseStream.Close();
                    this.ResponseStream.Dispose();
                    this.ResponseStream = null;
                }
            }
            catch (Exception ex) { }


            try{
                if (this.ResponseStreamReader != null)
                {
                    this.ResponseStreamReader.Close();
                    this.ResponseStreamReader.Dispose();
                    this.ResponseStreamReader = null;
                }
            }
            catch (Exception ex) { }

            SendDisconnectEvent();
            DisconnectRequested = false;
        }



        #region IDisposable Members

        public void Dispose()
        {

            this.Disconnect();
        }

        #endregion

    }
}
